package net.pimpas.framework;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import net.pimpas.configuration.PimpasFrameworkConfiguration;
import net.pimpas.context.DefaultFrameworkContext;
import net.pimpas.core.configuration.exception.ConfigurationException;
import net.pimpas.core.configuration.file.FileConfiguration;
import net.pimpas.core.context.FrameworkContext;
import net.pimpas.core.scanner.Scanner;
import net.pimpas.core.scanner.exception.ScannerException;
import net.pimpas.core.serviceprovider.ServiceProvider;
import net.pimpas.core.serviceprovider.instancetype.ServiceProviderInstanceType;
import net.pimpas.core.visitor.Visitor;
import net.pimpas.exception.PimpasFrameworkException;
import net.pimpas.scanner.ClassPathScanner;
import net.pimpas.scanner.PimpasFrameworkScanner;
import net.pimpas.util.reflection.ReflectionUtils;
import net.pimpas.visitor.FrameworkClassesVisitor;

import org.apache.log4j.Logger;

public class PimpasFramework {
	private static Logger logger = Logger.getLogger(PimpasFramework.class);

	private Map<Class<? extends ServiceProvider>, Class<? extends ServiceProvider>> servicesProvider;
	private Map<Class<? extends ServiceProvider>, ServiceProvider> singletons;
	private boolean configured;
	private List<Visitor> visitors;
	private FrameworkContext context;

	private static PimpasFramework pimpas; 
	public static PimpasFramework getInstance() {
		if(PimpasFramework.pimpas == null) {
			synchronized(PimpasFramework.class) {
				// still null?!
				if(PimpasFramework.pimpas == null) {
					PimpasFramework.pimpas = new PimpasFramework();
					try {
						PimpasFramework.pimpas.config();
					} catch (PimpasFrameworkException e) {
						System.out.println("Fatal Exception when trying to config Framework!");
						throw new RuntimeException(e);
					}
				}
			}
		}
		
		return PimpasFramework.pimpas;
	}
	
	private PimpasFramework() {
		this.setServicesProvider(new HashMap<Class<? extends ServiceProvider>, Class<? extends ServiceProvider>>());
		this.setSingletons(new HashMap<Class<? extends ServiceProvider>, ServiceProvider>());
		this.setVisitors(new ArrayList<Visitor>());
		this.setConfigured(false);
	}
	
	private Map<Class<? extends ServiceProvider>, Class<? extends ServiceProvider>> getServicesProvider() {
		return servicesProvider;
	}

	private void setServicesProvider(Map<Class<? extends ServiceProvider>, Class<? extends ServiceProvider>> serviceProviders) {
		this.servicesProvider = serviceProviders;
	}

	@SuppressWarnings("unchecked")
	public void config() throws PimpasFrameworkException {
		// get configuration
		if(this.isConfigured()) {
			logger.error("PimpasFramework was configured before!");
			return;
		}
		long startedConfig = System.currentTimeMillis();
		Scanner pimpasScan = new PimpasFrameworkScanner();
		
		try {
			pimpasScan.scan();
		} catch (ScannerException e1) {
			throw new PimpasFrameworkException(e1);
		}
		
		List<FileConfiguration> files = pimpasScan.getResults();
		PimpasFrameworkConfiguration pimpasConfig = new PimpasFrameworkConfiguration();
		pimpasConfig.setPimpas(this);
		pimpasConfig.getFilesConfiguration().addAll(files);
		
		try {
			pimpasConfig.consume();
		} catch (ConfigurationException e) {
			throw new PimpasFrameworkException("Unable to config Framework", e);
		}
		
		this.setConfigured(true);
	
		// run classpathscanner (getting visitors class)
        // add baseVisitor Class
        this.getVisitors().add(new FrameworkClassesVisitor());
		ClassPathScanner classPath = new ClassPathScanner();
		try {
			classPath.scan();
		} catch (ScannerException e1) {
			throw new PimpasFrameworkException(e1);
		}
		Collection<Class<? super Visitor>> visitors = FrameworkClassesVisitor.getVisitorClasses();
		this.getVisitors().clear();
		for(Class<? super Visitor> v: visitors)
			try {
				this.getVisitors().add((Visitor) v.newInstance());
			} catch (Exception e) {
				throw new PimpasFrameworkException("Unable to config Framework", e);
			}
			
		// run again classpath scanner
			try {
				classPath.scan();
			} catch (ScannerException e1) {
				throw new PimpasFrameworkException(e1);
			}
		// clear visitors
		this.getVisitors().clear();
		this.setVisitors(null);
		
		// check context, is null?!
		if(this.getContext() == null) {
			this.setContext(new DefaultFrameworkContext());
			logger.info("[PIMPASFRAMEWORK]: Context not found, using default context.");
		}
		
		long finishedConfig = System.currentTimeMillis();
		Calendar c = Calendar.getInstance();
		c.setTimeInMillis((finishedConfig-startedConfig));
		logger.debug("[PIMPASFRAMEWORK] TOTAL CONFIGURATION TIME: ("+c.get(Calendar.MINUTE)+
				" minutes and "+c.get(Calendar.SECOND)+" seconds and "+
				c.get(Calendar.MILLISECOND)+" milliseconds).");
		logger.debug("{PIMPAS FRAMEWORK READY}");
	}

	private boolean isConfigured() {
		return configured;
	}


	private void setConfigured(boolean configured) {
		this.configured = configured;
	}


	private Map<Class<? extends ServiceProvider>, ServiceProvider> getSingletons() {
		return singletons;
	}


	private void setSingletons(Map<Class<? extends ServiceProvider>, ServiceProvider> singletons) {
		this.singletons = singletons;
	}


	public List<Visitor> getVisitors() {
		return visitors;
	}


	private void setVisitors(List<Visitor> visitors) {
		this.visitors = visitors;
	}


	public FrameworkContext getContext() {
		return context;
	}


	public void setContext(FrameworkContext context) {
		this.context = context;
	}
	
	public <T extends ServiceProvider> void registerServiceProvider(Class<T> serviceProviderType, Class<T> serviceProvider) {
		if(!this.getServicesProvider().containsKey(serviceProviderType)) {
			// check interface
			if(net.pimpas.util.reflection.ReflectionUtils.isSubclass(serviceProvider, serviceProviderType)) {
				// check annotation
				if(net.pimpas.util.reflection.ReflectionUtils.classHaveAnnotation(serviceProvider, net.pimpas.core.serviceprovider.ann.ServiceProvider.class)) {
					logger.debug("Adding ServiceProvider => Type: "+serviceProviderType.getName()+", Provider: "+serviceProvider.getName());
					this.getServicesProvider().put(serviceProviderType, serviceProvider);
				} else
					logger.error("Can't add serviceProvider: {"+serviceProvider.getName()+"}, Reason: class miss Annotation ServiceProvider!");
			} else
				logger.error("Can't add serviceType: {"+serviceProviderType.getName()+"}, Reason: wrong classes type!");
		} else
			logger.error("Can't replace serviceProvider for type: {"+serviceProviderType.getName()+"}");
	}

	@SuppressWarnings("unchecked")
	public <T extends ServiceProvider> T getServiceProvider(Class<T> providerType, Object... params) {
		if(providerType == ServiceProvider.class) {
			logger.error("provider type {"+providerType.getName()+"} its not an subclass of {"+ServiceProvider.class.getName()+"}");
			return null;
		}
		
		Set set = this.getServicesProvider().entrySet();
		Iterator it = set.iterator();
		Class tmp = null;
		Entry entry = null;
		T provider = null;
		Constructor constructor = null;
		Constructor[] constructors = null;
		Class parameter = null;
		boolean parametersNotMatch = false;
		net.pimpas.core.serviceprovider.ann.ServiceProvider ann = null;
		ServiceProviderInstanceType instanceType = null;
		serviceProviderFind:
		while(it.hasNext()) {
			entry = (Entry) it.next();
			tmp = (Class) entry.getKey();
			if(tmp.equals(providerType)) {
				try {
					// check instance in singletons
					if(this.getSingletons().get(tmp) != null)
						return (T) this.getSingletons().get(tmp);
					
					constructors = ((Class)entry.getValue()).getConstructors();
					if(constructors.length > 0) {
						if(params.length > 0) {
							// try instance using parameters
							for(int i=0;i<constructors.length;i++) {
								constructor = constructors[i];
								if(constructor.getParameterTypes().length == params.length) {
									// check parameters
									parametersNotMatch = false;
									parametersFind:
									for(int j=0;j<constructor.getParameterTypes().length;j++) {
										parameter = constructor.getParameterTypes()[j];
										if(!ReflectionUtils.isSubclass(params[j].getClass(), parameter))
											parametersNotMatch = true;
											break parametersFind;
									}
									if(!parametersNotMatch) {
										provider = (T) constructor.newInstance(params);
										break serviceProviderFind;
									}
								}
							}
						}
					} else {
						logger.error("Can't find any constructor for provider {"+entry.getValue()+"}");
						throw new RuntimeException("Can't find any constructor for provider {"+entry.getValue()+"}");
					}
					
					// instance default constructor (no-parameters)
					provider = (T) ((Class)entry.getValue()).newInstance();
					break;
				} catch (Exception e) {
					logger.fatal("Unrecognized exception", e);
					throw new RuntimeException(e);
				}
			}
		}
		
		try {
			if(provider != null) {
				provider.config();
				// check singleton class
				ann = (net.pimpas.core.serviceprovider.ann.ServiceProvider) provider.getClass().getAnnotation(net.pimpas.core.serviceprovider.ann.ServiceProvider.class);
				instanceType = ann.instanceType();
				if(instanceType.equals(ServiceProviderInstanceType.SINGLETON))
					this.getSingletons().put(providerType, provider);
			} else
				logger.error("Can't find provider for provider type: {"+providerType.getName()+"}");
		} catch (ConfigurationException e) {
			logger.fatal("Unknown error while configuring provider {"+provider.getClass().getName()+"}", e);
			throw new RuntimeException(e);
		}
		
		return provider;
	}
}